package IC.SemanticChecks;

import IC.Constants;
import IC.AST.*;
import IC.SymbolTable.BlockSymbolTable;
import IC.SymbolTable.ClassSymbolTable;
import IC.SymbolTable.ExtendingClassSymbolTable;
import IC.SymbolTable.GlobalSymbolTable;
import IC.SymbolTable.SymbolKind;
import IC.SymbolTable.StaticMethodSymbolTable;
import IC.SymbolTable.Symbol;
import IC.SymbolTable.SymbolTable;
import IC.SymbolTable.VirtualMethodSymbolTable;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;


/**
 * a propagating visitor class that builds the symbol tables and the type table, given the AST.
 */
public class TablesBuilder implements PropagatingVisitor<SymbolTable, Type> {

	private String rootName;
	private SymbolTable root;
	private int appCounter = -1;
	
	//C'tor
	public TablesBuilder(){
	
	}
	
	public SymbolTable Build(Program program, String fileName) throws Exception {
		this.rootName = fileName;
		program.accept(this, null);
		return root;
	}
	
	private void Setter(ASTNode Node, SymbolTable Parent, SymbolTable Child) {		
		Node.setScope(Child);
		Parent.setChild(Child);
		Child.setParent(Parent);
	}
	
	public Type visit(Program program, SymbolTable d) throws Exception {
		SymbolTable root = new GlobalSymbolTable(rootName);
		program.setScope(root);
	
		for (ICClass c : program.getClasses()) 
			TypeTable.createClassType(c.getName(), c.getSuperClassName(), c);
		
		for (ICClass c : program.getClasses())
			c.accept(this, root);
		this.root =  root;
		return null;
	}

	
	public Type visit(ICClass icClass, SymbolTable d) throws Exception {
		SymbolTable result;
		if (icClass.hasSuperClass()) {
			if (icClass.getSuperClassName().equals(icClass.getName()))
				throw new SemanticError(SemanticErrorType.INVALID_BASE_CLASS,icClass.getLine());
			SymbolTable superClass = TypeTable.getClass(icClass.getSuperClassName());
			if (superClass == null)
				throw new SemanticError(SemanticErrorType.BASE_CLASS_UNDEFINED,icClass.getLine());
			result = new ExtendingClassSymbolTable(icClass.getName(), superClass);
			Setter(icClass, superClass, result);
		} 
		else 
		{
			result = new ClassSymbolTable(icClass.getName());
			Setter(icClass, d, result);
		}
		ClassType classType = TypeTable.classType(icClass.getName());
		if (classType == null) 
			throw new SemanticError(SemanticErrorType.INVALID_CLASS_EXT,icClass.getLine());
		Symbol symbol = new Symbol(icClass.getName(), classType, SymbolKind.CLASS);
		d.insertEntry(symbol, icClass.getLine());
		Symbol thisSymbol = new Symbol(Constants.THIS, classType, SymbolKind.THIS);
		result.insertEntry(thisSymbol, icClass.getLine());
		
		for (Field f : icClass.getFields())
			f.accept(this, result);
		
		for (Method m : icClass.getMethods()) 
			m.accept(this, result);
		return null;
	}

	
	public Type visit(Field field, SymbolTable d) throws Exception {
		field.setScope(d);
		Symbol symbol = new Symbol(field.getName(), field.getType().accept(this, null) , SymbolKind.FIELD);
		d.insertEntry(symbol, field.getLine());
		return null;
	}

	
	public Type visit(VirtualMethod method, SymbolTable d) throws Exception {
		appCounter = 1;
		SymbolTable result = new VirtualMethodSymbolTable(method.getName());
		Setter(method, d, result);
		MethodType methodType = TypeTable.createMethodType(this, method, result);
		Symbol symbol = new Symbol(method.getName(), methodType, SymbolKind.VIRTUAL_METHOD);
		d.insertEntry(symbol, method.getLine());
		
		for (Statement s : method.getStatements()) 
			s.accept(this, result);

		return null;
	}

	
	public Type visit(StaticMethod method, SymbolTable d) throws Exception {
		appCounter = 1;
		SymbolTable result = new StaticMethodSymbolTable(method.getName());
		Setter(method, d, result);
		MethodType methodType = TypeTable.createMethodType(this, method, result);
		Symbol symbol = new Symbol(method.getName(), methodType,SymbolKind.STATIC_METHOD);
		d.insertEntry(symbol, method.getLine());
		
		for (Statement s : method.getStatements()) 
			s.accept(this, result);
		return null;
	}

	
	public Type visit(LibraryMethod method, SymbolTable d) throws Exception {
		SymbolTable result = new StaticMethodSymbolTable(method.getName());
		Setter(method, d, result);
		MethodType methodType = TypeTable.createMethodType(this, method, result);
		Symbol symbol = new Symbol(method.getName(), methodType,SymbolKind.STATIC_METHOD);
		d.insertEntry(symbol, method.getLine());
		
		for (Statement s : method.getStatements()) 
			s.accept(this, result);
		
		return null;
	}
	
	
	public Type visit(Formal formal, SymbolTable d) throws Exception {
		formal.setScope(d);
		Type formalType = formal.getType().accept(this, null);
		Symbol symbol = new Symbol(formal.getName(), formalType, SymbolKind.FORMAL);
		d.insertEntry(symbol, formal.getLine());
		return formalType;
	}

	
	public Type visit(PrimitiveType type, SymbolTable d) throws Exception {
		type.setScope(d);
		Type result;
		switch (type.getDataType()) {
		case INT: 
			result = TypeTable.intType;
			break;
		case BOOLEAN:
			result = TypeTable.boolType;
			break;
		case STRING:
			result = TypeTable.stringType;
			break;
		case VOID:
			result = TypeTable.voidType;
			break;
		default:
			throw new SemanticError(SemanticErrorType.INVALID_PRIMITIVE_TYPE, type.getLine());
		}
		result = TypeTable.getArrayType(result, type.getDimension(), type.getLine());
		if (SemanticError.hasError()) 
			SemanticError.throwError();
		return result;
	}

	
	public Type visit(UserType type, SymbolTable d) throws Exception {
		type.setScope(d);
		ClassType classType = TypeTable.classType(type.getName());
		if (classType == null) 
			throw new SemanticError(SemanticErrorType.INVALID_CLASS, type.getLine());
		
		return TypeTable.getArrayType(classType, type.getDimension(), type.getLine());
	}
	
	
	public Type visit(StatementsBlock statementsBlock, SymbolTable d) throws Exception {
		String BlockName = "statement block in " + d.getDisplayId();
		SymbolTable result = new BlockSymbolTable(BlockName);
		Setter(statementsBlock, d, result);
		for (Statement s : statementsBlock.getStatements()) 
			s.accept(this, result);

		return null;
	}

	
	public Type visit(LocalVariable localVariable, SymbolTable d) throws Exception {
		localVariable.setScope(d);
		if (localVariable.hasInitValue()) 
			localVariable.getInitValue().accept(this, d);
		
		Type var = localVariable.getType().accept(this, d);
		Symbol symbol = new Symbol(localVariable.getName(), var, SymbolKind.LOCAL, appCounter++);
		d.insertEntry(symbol, localVariable.getLine());
		return null;
	}

	
	public Type visit(If ifStatement, SymbolTable d) throws Exception {
		ifStatement.setScope(d);
		ifStatement.getCondition().accept(this, d);
		ifStatement.getOperation().accept(this, d);
		
		if(ifStatement.hasElse()) 
			ifStatement.getElseOperation().accept(this, d);
		return null;
	}

	
	public Type visit(While whileStatement, SymbolTable d) throws Exception {
		whileStatement.setScope(d);
		whileStatement.getCondition().accept(this, d);
		whileStatement.getOperation().accept(this, d);
		return null;
	}
	
	
	public Type visit(Assignment assignment, SymbolTable d) throws Exception {
		assignment.setScope(d);
		assignment.getVariable().accept(this, d);
		assignment.getAssignment().accept(this, d);
		return null;
	}

	
	public Type visit(CallStatement callStatement, SymbolTable d) throws Exception {
		callStatement.setScope(d);
		callStatement.getCall().accept(this, d);
		return null;
	}

	
	public Type visit(Return returnStatement, SymbolTable d) throws Exception {
		returnStatement.setScope(d);
		
		if (returnStatement.hasValue()) 
			returnStatement.getValue().accept(this, d);
		return null;
	}
	
	
	public Type visit(Break breakStatement, SymbolTable d) {
		breakStatement.setScope(d);
		return null;
	}

	
	public Type visit(Continue continueStatement, SymbolTable d) {
		continueStatement.setScope(d);
		return null;
	}

	
	public Type visit(VariableLocation location, SymbolTable d) throws Exception {
		location.setAppearanceOrder(appCounter++);
		location.setScope(d);
		
        if (location.isExternal()) 
        	location.getLocation().accept(this, d);
       
		return null;
	}

	
	public Type visit(ArrayLocation location, SymbolTable d) throws Exception {
		location.setScope(d);
		location.getArray().accept(this, d);
		location.getIndex().accept(this, d);
		return null;
	}

	
	public Type visit(StaticCall call, SymbolTable d) throws Exception {
		call.setScope(d);
		
		for (Expression arg : call.getArguments()) 
			arg.accept(this, d);
		
		return null;
	}

	
	public Type visit(VirtualCall call, SymbolTable d) throws Exception {
		call.setScope(d);
		
		for(Expression arg : call.getArguments()) 
			arg.accept(this, d);
		
		if (call.isExternal()) 
			call.getLocation().accept(this, d);
		
		return null;
	}

	
	public Type visit(This thisExpression, SymbolTable d) {
		thisExpression.setScope(d);
		return null;
	}

	
	public Type visit(NewClass newClass, SymbolTable d) {
		newClass.setScope(d); 
		return null;
	}

	
	public Type visit(NewArray newArray, SymbolTable d) throws Exception {
		newArray.setScope(d);   
        newArray.getType().accept(this, d);
        newArray.getSize().accept(this, d);
		return null;
	}

	
	public Type visit(Length length, SymbolTable d) throws Exception {
		length.setScope(d);
		length.getArray().accept(this, d);
		return null;
	}

	
	public Type visit(MathBinaryOp binaryOp, SymbolTable d) throws Exception {
		binaryOp.setScope(d);   
		binaryOp.getFirstOperand().accept(this, d);
		binaryOp.getSecondOperand().accept(this, d);
		return null;
	}

	
	public Type visit(LogicalBinaryOp binaryOp, SymbolTable d) throws Exception {
		binaryOp.setScope(d);   
		binaryOp.getFirstOperand().accept(this, d);
		binaryOp.getSecondOperand().accept(this, d);
		return null;
	}

	
	public Type visit(MathUnaryOp unaryOp, SymbolTable d) throws Exception {
		unaryOp.setScope(d);   
		unaryOp.getOperand().accept(this, d);
		return null;

	}

	
	public Type visit(LogicalUnaryOp unaryOp, SymbolTable d) throws Exception {
		unaryOp.setScope(d);   
	    unaryOp.getOperand().accept(this, d);
		return null;
	}

	
	public Type visit(Literal literal, SymbolTable d) {
		literal.setScope(d);
		return null;
	}

	
	public Type visit(ExpressionBlock expressionBlock, SymbolTable d) throws Exception {
		expressionBlock.setScope(d); 
        expressionBlock.getExpression().accept(this, d);
		return null;
	}
	
}
